package config

import (
	"fmt"
	"path/filepath"

	"github.com/spf13/viper"
)

// The public member is shared within entire application.
var ConfigLoader AppConfigLoader

// This is an example of the data structure for config file.
// The structure is very complicated you might want to get the value via key.
type AppConfig struct {
	DataSource struct {
		Dsn string `mapstructure:"dsn"`
	} `mapstructure:"data_source"`

	Jwt struct {
		SigningKey string `mapstructure:"signing_key"`
		Expiration int64  `mapstructure:"expiration"`
	} `mapstructure:"jwt"`

	Server struct {
		RouteRootGroupName           string `mapstructure:"route_root_group_name"`
		RouteApiGroupName            string `mapstructure:"route_api_group_name"`
		DefaultRequestTimeoutSeconds int    `mapstructure:"default_request_timeout_seconds"`
		Port                         string `mapstructure:"port"`
	} `mapstructure:"server"`

	AppConfig struct {
		RecentSearchListMaxCount int `mapstructure:"recent_search_list_max_count"`
	} `mapstructure:"app_config"`

	Resource struct {
		Upload struct {
			RootFilePath string `mapstructure:"root_file_path"`
		} `mapstructure:"upload"`
		StaticResourcesUrl  string `mapstructure:"static_resources_url"`
		HttpBaseUrl         string `mapstructure:"http_base_url"`
		BankLogoUrl         string `mapstructure:"bank_logo_url"`
		RegionFlagUrl       string `mapstructure:"region_flag_url"`
		ApkDirectoryName    string `mapstructure:"apk_directory_name"`
		AvatarDirectoryName string `mapstructure:"avatar_directory_name"`
		H5DirectoryName     string `mapstructure:"h5_directory_name"`
		DefaultAvatarUrl    string `mapstructure:"default_avatar_url"`
	} `mapstructure:"resource"`

	BaiduMap struct {
		AndroidKey string `mapstructure:"android_key"`
	} `mapstructure:"baidu_map"`

	AMap struct {
		RestApiUrl string `mapstructure:"rest_api_url"`
		AndroidKey string `mapstructure:"android_key"`
		WebApiKey  string `mapstructure:"web_api_key"`
	} `mapstructure:"a_map"`

	Timezone struct {
		DefaultLocation string `mapstructure:"default_location"`
	} `mapstructure:"timezone"`
}

// Provides public methods to interact with config file.
type AppConfigLoader interface {
	GetString(key string) string
	GetInt(key string) int
	GetBool(key string) bool
	GetAllSettings() map[string]any
	GetAppConfig() AppConfig
}

// Creates the instance of appConfigService.
// It is called by Init function.
// It loads the config file during initialization.
func NewAppConfigLoader() AppConfigLoader {
	var appConfig AppConfig
	var pathExecutable string

	// Running in GoLand GetExecutableDirectory() returns path like "C:\Users\{username}\AppData\Local\Temp\GoLand".
	// The path points to your system temp file directory. That makes conf directory not found and project startup failed.
	//
	// Running in VS Code GetExecutableDirectory() returns path like "D:\Dev\Workspace\Git\project-frameworks\family-expense\server\go-gin\family-expense-api".
	// This path is exactly where your project source code sits. It makes conf directory available.
	//
	// That's why we need to set pathExecutable with different value based on EnvMode to make project work both in GoLand & VS Code when debugging.
	if SysEnvMode.IsDevMode() {
		// "." gets current working directory just like os.Getwd() does.
		// So you have to understand what it means.
		// For example:
		// You executable full path is /home/family-expense/api/family-expense-api.
		// When you are under /home/family-expense/api and running family-expense-api everything works great.
		// But if you are out of this directory, let's say you are under /home, and you are trying to do "root:/home# ./family-expense/api/family-expense-api".
		// Then this program won't work and complains the path issue.
		pathExecutable = "../../"
	} else {
		// Gets the directory name that startedd the current process.
		pathExecutable, _ = GetExecutableDirectory()

		// Since executable is in /cmd/server then we need to get 2 levels up.
		pathExecutable = filepath.Dir(filepath.Dir(pathExecutable))
	}

	baseConfigFilePath := filepath.Join(pathExecutable, "internal", "config", "config.yaml")
	envConfigFilePath := filepath.Join(pathExecutable, "internal", "config", fmt.Sprintf("config.%v.yaml", SysEnvMode.GetCurrentMode()))

	// Loads base config first
	viper.SetConfigFile(baseConfigFilePath)

	if err := viper.ReadInConfig(); err != nil {
		fmt.Printf("Error opening base config file: %v\n", err)
	}

	// Loads environment-specific config
	viper.SetConfigFile(envConfigFilePath)

	// Merges environment-specific config.
	if err := viper.MergeInConfig(); err != nil {
		fmt.Printf("Error merging env config file: %v\n", err)
	}

	// Unmarshal the config data to structure type.
	// This is another way you can access the config data.
	if err := viper.Unmarshal(&appConfig); err != nil {
		fmt.Printf("Error unmarshaling config: %v\n", err)
	}

	return &appConfigLoader{appConfig: appConfig}
}

// GetString returns the value associated with the key as a string.
// The nested key is supported. Example: dataSource.dsn.
func (*appConfigLoader) GetString(key string) string {
	return viper.GetString(key)
}

// GetInt returns the value associated with the key as an integer.
// The nested key is supported. Example: dataSource.dsn.
func (*appConfigLoader) GetInt(key string) int {
	return viper.GetInt(key)
}

// GetBool returns the value associated with the key as a boolean.
// The nested key is supported. Example: dataSource.dsn.
func (*appConfigLoader) GetBool(key string) bool {
	return viper.GetBool(key)
}

// AllSettings merges all settings and returns them as a map[string]any.
func (*appConfigLoader) GetAllSettings() map[string]any {
	return viper.AllSettings()
}

// GetAppConfig returns the data structure of AppConfig.
func (s *appConfigLoader) GetAppConfig() AppConfig {
	return s.appConfig
}

type appConfigLoader struct {
	// The private member used to store the loaded config data.
	appConfig AppConfig
}
